#!/usr/bin/env python
# @Date    : 2022-03-24
# @Author  : Bright (brt2@qq.com)
# @Link    : https://gitee.com/brt2
# @Version : 0.2.6

import subprocess
import os
import os.path as osp
import shutil
import json
import re
from glob import iglob
from time import time
# from queue import LifoQueue, PriotyQueue
from collections import deque
# from ast import literal_eval

import flask as web
from flask import Flask as Application
from flask import url_for, redirect, request

from mkm import dump_km_as_mkm, load_mkm_as_km
from _util import JsonConfigLoader

#####################################################################

REMOVE_TO_FOLDER = "__pycache__"  # False
CONFIG = JsonConfigLoader("config.json", curfile=__file__).load(template_file="config.template")
IPADDR_ALLOWED = CONFIG.get("IPADDR_ALLOWED", ["127.0.0.1"])
SHARED_FOLDERS = CONFIG.get("SHARED_FOLDERS", [])

app = Application(__name__, template_folder='static')

# from flask_cors import CORS
# CORS(app, resources={r"/*": {"origins": "*"}})

# 解决缓存问题
from datetime import timedelta
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = timedelta(seconds=1)

# 文件管理器Application
REPO_DIR = osp.relpath(osp.join(osp.dirname(__file__), ".."))
ROOT_DIR = osp.join(REPO_DIR, "mindmap")
if not osp.exists(ROOT_DIR):
    os.makedirs(ROOT_DIR)
    shutil.copy(osp.join(REPO_DIR, "template/__demo__.mkm"), osp.join(ROOT_DIR, "__demo__.mkm"))

_template_filelist = """
<ul>
    {% for path, fn in files %}
    <li><a href="/listdir/{{path}}">{{fn}}</a></li>
    {% endfor %}
</ul>
<script src="/static/bower_components/jquery/dist/jquery.js"/></script>
<script src="/static/search.js"></script>
"""
template_filesys = """
<!doctype html>
<title>我的脑图 - KityMind</title>
<h2>文件列表:</h2>
<hr>
""" + _template_filelist

template_search_results = """
<!doctype html>
<title>脑图检索: {{keyword}}</title>
<h2>搜索结果: </h2>
<hr>

<ul>
    {% for path, contents in files.items() %}
    <li><a href="/listdir/{{path}}">{{path}}</a></li>
        <ol>
            {% for line in contents %}
            <li>{{line}}</li>
            {% endfor %}
        </ol>
    {% endfor %}
</ul>
<script>
var $ = document.querySelectorAll.bind(document)

var s = "{{keyword}}";
$('ol li').forEach(function(curr){
    let html = curr.innerHTML
    html = html.replace(new RegExp(s,"gi"), s.fontcolor("red"));
    curr.innerHTML = html
})
</script>
<script src="/static/bower_components/jquery/dist/jquery.js"/></script>
<script src="/static/search.js"></script>
"""
# + _template_filelist

# var html = document.body.innerHTML;
# html = html.replace(new RegExp(s,"g"), s.fontcolor("red"));
# document.body.innerHTML = html;

EXCLUDE_DIR = ["__pycache__", ".git", ".gitignore"]
SEARCH_DATA = deque(maxlen=5)

@app.before_request
def limit_ipaddr():
    if "*" in IPADDR_ALLOWED:
        return

    try:
        _, type_, rpath_mkm = request.path.split("/", 2)
    except ValueError:  # /favicon.ico
        return

    if type_ != "listdir":
        return
    else:
        # if rpath_mkm in SHARED_FOLDERS:
        for shared_allow in SHARED_FOLDERS:
            # same_prefix = osp.commonprefix([rpath_mkm, shared_allow])
            # if same_prefix in shared_allow:
            if shared_allow in rpath_mkm:  # 简化运算，并留一个bug，以允许子层shared目录生效
                return

    if request.remote_addr not in IPADDR_ALLOWED:
        app.logger.warning(f">>> Limit IP: {request.remote_addr}")
        web.abort(403)  # 返回"Forbidden"错误

@app.route('/')
def root():
    return redirect(url_for("listdir", path="root"))

@app.route('/listdir/<path:path>')
def listdir(path):
    if path == "favicon.ico":
        return "..."
    elif path == "root":
        root = ROOT_DIR
        path = ""
    else:
        root = osp.join(ROOT_DIR, path)

    if osp.isdir(root):
        args = [(osp.join(path, fn), fn) for fn in sorted(os.listdir(root)) if fn not in EXCLUDE_DIR]
        return web.render_template_string(template_filesys, files=args)
    else:
        return open_mindmap(root)

def open_mindmap(path_file):
    path_fn, ext = osp.splitext(path_file)
    if ext == ".km":
        try:
            with open(path_file, "r", encoding="utf8") as fp:
                content = fp.read()
        except UnicodeDecodeError:
            with open(path_file, "r", encoding='gb18030', errors='ignore') as fp:
                content = fp.read()
    elif ext == ".mkm":
        try:
            dict_km = load_mkm_as_km(path_file)
            content = json.dumps(dict_km, separators=(",",":"))
        except Exception as e:
            print("[!] MkmDecodeError:", e)
    else:
        # print(">>>", path_file)
        raise Exception(f"Unknown-type【{ext}】")
    # print(">>>", content)
    filename = osp.basename(path_fn)
    return web.render_template("index.html", title=filename, data_json=content)

# @app.post("/new_kmfile/")
@app.route("/new_kmfile/", methods=['POST'])
def new_kmfile():
    filename = request.form.get("filename")
    path_file = osp.join(ROOT_DIR, filename + ".mkm")
    with open(path_file, "w", encoding="utf8") as fp:
        fp.write("{}")
    return {"status": "[+] 文件创建成功"}

@app.route("/delete_kmfile/", methods=['POST'])
def delete_kmfile():
    filename = request.form.get("filename")
    path_file = osp.join(ROOT_DIR, filename)
    if REMOVE_TO_FOLDER:
        move_to = osp.join(osp.dirname(path_file), REMOVE_TO_FOLDER)
        if not osp.exists(move_to):
            os.makedirs(move_to)
        path_target = osp.join(move_to, osp.basename(filename))
        if osp.exists(path_target):
            os.remove(path_target)
        shutil.move(path_file, move_to)
    else:
        os.remove(path_file)
    return {"status": "[+] 文件删除成功"}

def _search_by_grep(q, root_dir):

    def shell(str_cmd):
        # proc = await asyncio.create_subprocess_shell(str_cmd,
        #                         stdout = asyncio.subprocess.PIPE,
        #                         cwd = ROOT_DIR)
        # stdout, _ = await proc.communicate()
        proc = subprocess.run(str_cmd, stdout=subprocess.PIPE, shell=True, cwd=root_dir, check=False)
        # bytes_results = proc.stdout
        return proc.stdout

    # str_cmd = f"grep -RiI -c --include=*.km \"{q}\" | grep -v :0"
    # str_cmd = f"grep -RiI --include={{*.mkm,*.km}} \"{q}\""  # ??
    str_cmd = f"grep -RiI --exclude-dir={{__pycache__,.git}} \"{q}\""
    # str_cmd = f"rg -C1 -F -i \"{q}\" -g '*.mkm'"  # 尝试使用rigrep查询。实际上grep索引mkm文本效率并不低
    bytes_results = shell(str_cmd)

    return [p for p in bytes_results.decode().split("\n") if p]

def _grep_dir(q, root_dir):
    # 输出格式，参考ripgrep
    q = q.lower()
    out_dict = {}

    glob_pattern = os.path.join(root_dir, "**/*.mkm")
    for path_file in iglob(glob_pattern, recursive=True):
        match_lines = []  # line: content
        with open(path_file, "r", encoding="utf-8") as fp:
            for i, line in enumerate(fp.readlines()):
                if q in line.lower():
                    match_lines.append(f"{i}. {line}")  # 输出样式
        if match_lines:
            rpath = os.path.relpath(path_file, root_dir)
            out_dict[rpath] = match_lines

    return out_dict

@app.route("/search")
def search():
    """ 异步处理查询 """
    _time0 = time()
    q = request.args.get("q")
    q = q.replace(" ", r"\s*")  # 替换空格

    # results = _search_by_grep(q, ROOT_DIR)
    match_files = _grep_dir(q, ROOT_DIR)

    SEARCH_DATA.append((q, match_files))  # [keyword, dict_search]
    app.logger.debug(f">>> Search Keywords - 耗时统计: {time() - _time0}")
    return {"status": "ok" if match_files else "null"}

@app.route("/search_results/<query>")
def search_results(query):
    # 遍历LifoQueue
    out_dict = {}
    for keyword, match_files in reversed(SEARCH_DATA):  # 倒序
        if keyword == query:
            out_dict = match_files
            break
    return web.render_template_string(template_search_results, keyword=query, files=out_dict)

@app.route("/save_as/", methods=['POST'])
def save_as():
    data = request.form.get('data')
    path = request.form.get("path")
    # 备份为mkm
    abspath = osp.join(ROOT_DIR, path)
    type_ = request.form.get("type")
    path_save = osp.splitext(abspath)[0] + "." + type_
    # print(">>>", path_save)
    # print(">>>", data)
    if type_ == "km":
        with open(path_save, "w", encoding="utf8") as fp:
            fp.write(data)
    elif type_ == "mkm":
        dict_data = json.loads(data)
        # dict_data = eval(data)
        # dict_data = literal_eval(data)
        dump_km_as_mkm(dict_data, path_save)
    elif type_ == "md":
        json_data = json.loads(data)
        dump_as_markdown(json_data, path_save)
    else:
        return {"unknown-type": type_}
    return {"saved": path_save}

def dump_as_markdown(data, path_save):
    """ 用于导出为日志 """
    list_md = []

    def dump_node(dict_node, indent, is_work_items=False):
        text = dict_node["data"]["text"]
        is_date = re.match(r"\d{2}-\d{2}", text)

        # note = dict_node["data"].get("note")
        priority = dict_node["data"].get("priority")
        background = dict_node["data"].get("background")
        font_weight = dict_node["data"].get("font-weight")
        if priority or background or font_weight:
            text = f"**{text}**"
        if is_work_items:
            progress = dict_node["data"].get("progress")
            text = f"+ [{'x' if progress == 9 else ' '}] " + text

        list_md.append(indent + text)

        for child in dict_node["children"]:
            dump_node(child, indent+"  ", is_date)

    dump_node(data["root"], indent="")
    # print("\n".join(list_md))
    with open(path_save, "w", encoding="utf8") as fp:
        fp.write("\n".join(list_md))


if __name__ == "__main__":
    # export FLASK_ENV=development
    # Run: flask run -h '0.0.0.0' -p 1124
    def export_log(path_log):
        import logging

        handler = logging.FileHandler(path_log, encoding='UTF-8')
        handler.setFormatter(logging.Formatter("[%(levelname)s  %(name)s] ---- %(message)s"))
        handler.setLevel(logging.DEBUG)

        app.logger.addHandler(handler)
        app.logger.setLevel(logging.DEBUG)

        from flask.logging import default_handler  # <StreamHandler <stderr> (NOTSET)>
        # default_handler.setLevel(logging.WARNING)
        app.logger.removeHandler(default_handler)
        # app.logger.debug(app.logger.handlers)

    # export_log(os.path.expanduser("~/.cache/kitymind.log"))

    #####################################################################
    logger = app.logger
    # app.debug = True
    kwargs = CONFIG.get("KWARGS", {})

    try:
        from gevent import monkey
        monkey.patch_all()

        from gevent.pywsgi import WSGIServer
        logger.info("[+] 使用WSGIServer启动服务")
        WSGIServer((kwargs["host"], kwargs["port"]), app).serve_forever()
    except ImportError:
        try:
            from waitress import serve
            logger.info("[+] 使用waitress启动服务")
            if "debug" in kwargs:  # waitress不支持debug参数
                del kwargs["debug"]
            serve(app, **kwargs)  # threads=MAX_THREADS
        except ImportError:
            logger.info("[+] 使用FlaskServer启动服务【dev环境】")
            app.run(**kwargs)
